/*
 * Created by Jaren at 2023/2/8 20:55
 */
import 'dart:async';
import 'dart:io';

import 'package:inmotion_protocol/base/file/file_manager.dart';
import 'package:inmotion_protocol/common/exceptions.dart';
import 'package:inmotion_protocol/common/log_file_manager.dart';
import 'package:inmotion_protocol/common/vehicle_info.dart';
import 'package:inmotion_protocol/ezcan/ezcan_file_download_manager.dart';
import 'package:inmotion_protocol/ezcan/ezcan_message.dart';
import 'package:inmotion_protocol/ezcan/factory/ezcan_factory.dart';
import 'package:inmotion_protocol/ezcan/util.dart';
import 'package:inmotion_protocol/protocol_worker.dart';
import 'package:inmotion_protocol/util/byte_util.dart';
import 'package:inmotion_protocol/util/log.dart';

class EzCANLogFileManager extends LogFileManager {
  EzCANLogFileManager(super.protocol);

  @override
  LogConfig createLogConfig(String localFilePath, {int? target, ProductProperty? productProperty}) {
    if (productProperty == null) throw InvalidParamException("Required product property to create a log config.");
    return EzCANLogConfig(File(localFilePath));
  }

  @override
  IFileManager createFileDownloadManager(LogConfig config) {
    return EzCANSingleLogFileManager(protocol);
  }
}

class EzCANSingleLogFileManager extends EzCANFileDownloadManager {
  static const _tag = "EzCANSingleLogFileManager";

  EzCANLogConfig? _config;

  final FileContext _context = FileContext();

  EzCANSingleLogFileManager(super.protocol);

  StreamController<FileEvent> streamController = StreamController();

  @override
  void init(FileConfig config) {
    reset();
    if (config is! EzCANLogConfig) {
      throw InvalidParamException('Config should be an instance of EzCANLogConfig.');
    }
    _config = config;
    if (_config!.localFile.existsSync()) {
      _config!.localFile.deleteSync();
    }
    _config!.localFile.create(recursive: true);
    LogUtils.d(_tag, "init with config " + config.toString());
  }

  @override
  int sizePeerPkg() {
    return 150;
  }

  @override
  Stream<FileEvent> start() {
    _context.reset();

    _getLogFileInfo().then((totalSize) {
      if (checkInterrupted()) throw CanceledError();

      _context.totalSize = totalSize;
      return _openFile();
    }).catchError((error) {
      streamController.addError(error);
      return Future.value(false);
    }).then((shouldContinue) {
      if (shouldContinue) {
        streamController.add(FileOpenEvent(FileEvent.fileOpened));
        return _getFileContent();
      }
      return Future.value(false);
    }).catchError((error) {
      streamController.addError(error);
      return Future.value(false);
    }).then((shouldContinue) {
      if (shouldContinue) {
        streamController.add(FileOpenEvent(FileEvent.fileChecked));
        return _closeFile();
      }
      return Future.value(false);
    }).catchError((error) {
      if (error is CanceledError) {
        streamController.addError(error);
        return Future.value(false);
      }
      // ignore close file error
      return Future.value(true);
    }).then((shouldContinue) {
      if (shouldContinue) {
        streamController.add(FileOpenEvent(FileEvent.fileClosed));
      }
    });

    return streamController.stream;
  }

  Future<int> _getLogFileInfo() {
    return protocol.protocolWorker().sendAndGetRsp(EzcanFactory.genGetLogFileInfoMsg(_config!.logType, _config!.logId), maxRetryTimes: 3).then((rspMsg) {
      rspMsg as EzcanMessage;
      return ByteUtil.bytes2Uint32Le(rspMsg.extra!, 0).value;
    });
  }

  Future<bool> _openFile() {
    return protocol.protocolWorker().sendAndGetRsp(EzcanFactory.genOpenLogFileInfoMsg(_config!.logType, _config!.logId), timeoutMs: 1500, maxRetryTimes: 3).then((rspMsg) {
      return EzcanUtil.parseSettingsRsp(rspMsg as EzcanMessage).isSuccess();
    }).onError((error, stackTrace) {
      if (error is MsgTimeoutException) {
        throw FileOperationStepError(IFileManager.stepOpenFile, IFileManager.errorTimeout);
      }
      if (error is FileOperationStepError) {
        throw error;
      }
      throw FileOperationStepError(IFileManager.stepOpenFile, IFileManager.errorUnknown);
    });
  }

  Future<bool> _getFileContent() {
    return _getFileContentImpl(_context.offset, nextPkgSize(0)).catchError((error) {
      if (error is MsgTimeoutException) {
        throw FileOperationStepError.withExtra(IFileManager.stepTransferFile, IFileManager.errorTimeout, _context.offset);
      }
      if (error is FileOperationStepError) {
        throw error;
      }
      throw FileOperationStepError.withExtra(IFileManager.stepTransferFile, IFileManager.errorUnknown, _context.offset);
    });
  }

  Future<bool> _getFileContentImpl(int offset, int size) {
    return protocol.protocolWorker().sendAndGetRsp(EzcanFactory.genGetLogFileContentMsg(offset, size), timeoutMs: 1500, maxRetryTimes: 3).then((rspMsg) {
      rspMsg as EzcanMessage;
      if (rspMsg.extra == null) return Future.value(true);

      if (checkInterrupted()) throw CanceledError();

      _config!.localFile.writeAsBytesSync(rspMsg.extra!, mode: FileMode.writeOnlyAppend, flush: true);

      onTransferring(offset, rspMsg.extra!.length, _context.totalSize);

      if (!streamController.isClosed && !streamController.isPaused) {
        streamController.add(FileProgressEvent(offset + rspMsg.extra!.length, _context.totalSize, calAveSpeed(), calRtSpeed(), now() - startedTimestamp, calRemainSeconds()));
      }

      int _nextPkgSize = nextPkgSize(rspMsg.extra!.length);

      if (_nextPkgSize == 0) return Future.value(true);

      _context.offset += rspMsg.extra!.length;
      return _getFileContentImpl(_context.offset, _nextPkgSize);
    });
  }

  Future<bool> _closeFile() {
    return protocol.protocolWorker().sendAndGetRsp(EzcanFactory.genCloseLogFileInfoMsg(_config!.logType), maxRetryTimes: 3).then((rspMsg) {
      return EzcanUtil.parseSettingsRsp(rspMsg as EzcanMessage).isSuccess();
    });
  }

  int nextPkgSize(int currentPkgSize) {
    int remain = _context.totalSize - _context.offset - currentPkgSize;
    int _sizePeerPkg = sizePeerPkg();
    return remain > _sizePeerPkg ? _sizePeerPkg : remain;
  }
}

class FileContext {
  int totalSize = 0;
  int offset = 0;

  void reset() {
    totalSize = 0;
    offset = 0;
  }
}

class EzCANLogConfig extends LogConfig {
  static const logTypeNormal = 0;
  static const logTypeError = 1;

  /// V5、V8中存在普通日志和错误日志
  /// V10Like中仅有错误日志
  /// 目前在V5和V8中也仅获取错误日志
  int logType = logTypeError;

  /// 日志ID，一般默认为0
  int logId = 0;

  EzCANLogConfig(File localFile) {
    this.localFile = localFile;
  }
}
